Introduction to JuMP for solving power system problems

Objectives

In this section we will introduce Julia's JuMP package and consider its application in power systems using a 3 bus example. These applications include the economic dispatch, dc- and ac-based optimal power flow and dc-based unit commitment models. The learning objectives of this section include:

  • Economic dispatch: simple implementation
  • Unit commitment: impact of binary decisions
  • Optimal power flow: implementation and sensitivity of the dc-based model
  • Illustrative example

    In the following notes for the sake of simplicity, we are going to use a three bus example mirroring the interface between Western and Eastern Texas. This example is taken from R. Baldick, "Wind and Energy Markets: A Case Study of Texas," IEEE Systems Journal, vol. 6, pp. 27-34, 2012.

    For this example, we set the following characteristics of generators, transmission lines, wind farms and demands:

    Generator 1 Generator 2
    $g_{min}$, MW 0 300
    $g_{max}$, MW 1000 1000
    $c^g$, \$/MWh 50 100
    $c^{g0}$, \$/MWh 1000 0
    Line 1 Line 2
    $f^{max}$, MW 100 1000
    x, p.u. 0.001 0.001
    Wind farm 1 Wind farm 2
    $w^{f}$, MW 150 50
    $c^{w}$, \$/MWh 50 50
    Bus 1 Bus 2 Bus 3
    $d$, MW 0 0 15000

    Economic dispatch

    Economic dispatch (ED) is an optimization problem that minimizes the cost of supplying energy demand subject to operational constraints on power system assets. In its simplest modification, ED is an LP problem solved for an aggregated load and wind forecast and for a single infinitesimal moment. Mathematically, the ED problem can be written as follows: $$ \min \sum_{i \in I} c^g_{i} \cdot g_{i} + c^w \cdot w, $$ where $c_{i}$ and $g_{i}$ are the incremental cost ($\$/MWh$) and power output ($MW$) of the $i^{th}$ generator, respectively, and $c^w$ and $w$ are the incremental cost ($\$/MWh$) and wind power injection ($MW$), respectively.

    s.t.

  • Minimum ($g^{\min}$) and maximum ($g^{\max}$) limits on power outputs of generators:
  • $$ g^{\min}_{i} \leq g_{i} \leq g^{\max}_{i}. $$

  • Constraint on the wind power injection:
  • $$ 0 \leq w \leq w^f, $$ where $w$ and $w^f$ are the wind power injection and wind power forecast, respectively.

  • Power balance constraint:
  • $$ \sum_{i \in I} g_{i} + w = d^f, $$ where $d^f$ is the demand forecast.

    Further reading on ED models can be found in A. J. Wood, B. F. Wollenberg, and G. B. Sheblé, "Power Generation, Operation and Control", Wiley, 2013.

    Jump Implementation of Economic Dispatch

    
    
    In [ ]:
    # Define the packages 
    using JuMP # used for mathematical programming
    using Interact # used for enabling the slider
    using Gadfly # used for plotting 
    
    # Note that it is enough to define the packages once at the beginning of each session. 
    # No need to duplicate the definition of packages in every cell
    
    
    
    In [ ]:
    # Define some input data about the test system
    # Maximum power output of generators
    const g_max = [1000 1000];
    # Minimum power output of generators
    const g_min = [0 300];
    # Incremental cost of generators 
    const c_g = [50 100];
    # Fixed cost of generators
    const c_g0 = [1000 0]
    # Incremental cost of wind generators
    const c_w = 50;
    # Total demand
    const d = 1500;
    # Wind forecast
    const w_f = 200;
    
    
    
    In [ ]:
    # In this cell we create  function solve_ed, which solves the economic dispatch problem for a given set of input parameters.
    function solve_ed (g_max, g_min, c_g, c_w, d, w_f)
    #Define the economic dispatch (ED) model
    ed=Model() 
        
    # Define decision variables    
    @defVar(ed, 0 <= g[i=1:2] <= g_max[i]) # power output of generators
    @defVar(ed, 0 <= w  <= w_f ) # wind power injection
    
    # Define the objective function
    @setObjective(ed,Min,sum{c_g[i] * g[i],i=1:2}+ c_w * w)
    
    # Define the constraint on the maximum and minimum power output of each generator
    for i in 1:2
        @addConstraint(ed,  g[i] <= g_max[i]) #maximum
        @addConstraint(ed,  g[i] >= g_min[i]) #minimum
    end
    
    
    # Define the constraint on the wind power injection
    @addConstraint(ed, w <= w_f)
    
    # Define the power balance constraint
    @addConstraint(ed, sum{g[i], i=1:2} + w == d)
    
    # Solve statement
    solve(ed)
        # return the optimal value of the objective function and its minimizers
        return getValue(g), getValue(w), w_f-getValue(w), getObjectiveValue(ed)
    end
    
    # Solve the economic dispatch problem
    (g_opt,w_opt,ws_opt,obj)=solve_ed (g_max, g_min, c_g, c_w, d, w_f);
    
    println("\n")
    println("Dispatch of Generators: ", g_opt[i=1:2], " MW")
    println("Dispatch of Wind: ", w_opt, " MW")
    println("Wind spillage: ", w_f-w_opt, " MW") 
    println("\n")
    println("Total cost: ", obj, "\$")
    

    Economic dispatch with adjustable incremental costs

    In the following exercise we adjust the incremental cost of generator G1 and observe its impact on the total cost by using the manipulator

    
    
    In [ ]:
    # This cell uses the package Interact defined above. 
    # In this cell we create a manipulator that solves the economic dispatch problem for different values of c_g1_scale.
    
    @manipulate for c_g1_scale = 0.5:0.01:3.0
        c_g_scale = [c_g[1]*c_g1_scale  c_g[2]] # update the incremental cost of the first generator at every iteration
        g_opt,w_opt,ws_opt,obj = solve_ed(g_max, g_min, c_g_scale, c_w, d, w_f) # solve the ed problem with the updated incremental cost
        html("Dispatch of Generators, MW: $(g_opt[:])<br>"*
        "Dispatch of Wind, MW: $w_opt<br>"*
        "Spillage of Wind, MW: $ws_opt<br>"*
        "Total cost, \$: $obj")
    end
    

    Impact of the wind generation cost

    In the following exercise we introduce a new manipulator to vary the cost of wind generation and observe its impact the total cost, dispatch of generators G1 and G2, utilization of available wind under different values of the incremental cost of generator G1.

    
    
    In [ ]:
    @manipulate for c_w_scale = 1:0.1:3.5
        # Define the vectors of outputs
        obj_out = Float64[] 
        w_out = Float64[]
        g1_out = Float64[]
        g2_out = Float64[]
        
        @time for c_g1_scale = 0.5:0.01:3.0
            c_g_scale = [c_g[1]*c_g1_scale  c_g[2]] # update the incremental cost of the first generator at every iteration
            g_opt,w_opt,ws_opt,obj = solve_ed(g_max, g_min, c_g_scale, c_w_scale*c_w, d, w_f) # solve the ed problem with the updated incremental cost
            # Add the solution of the economic dispatch problem to the respective vectors
            push!(obj_out,obj)
            push!(w_out,w_opt)
            push!(g1_out,g_opt[1])
            push!(g2_out,g_opt[2])
        end
        
        # Plot the outputs
        # Define the size of the plots
        set_default_plot_size(16cm, 30cm)
        
        vstack(
        # Plot the total cost
        plot(x=0.5:0.01:3.0,y=obj_out, Geom.line,
        Guide.XLabel("c_g1_scale"), Guide.YLabel("Total cost, \$"),
        Scale.y_continuous(minvalue=50000, maxvalue=200000)),
        # Plot the power output of Generator 1
        plot(x=0.5:0.01:3.0,y=[g1_out], Geom.line,
        Guide.XLabel("c_g1_scale"), Guide.YLabel("Dispatch of  G1, MW"),
        Scale.y_continuous(minvalue=0, maxvalue=1100)),
        # Plot the power output of Generator 2    
        plot(x=0.5:0.01:3.0,y=[g2_out], Geom.line,
        Guide.XLabel("c_g1_scale"), Guide.YLabel("Dispatch of  G2, MW"),
        Scale.y_continuous(minvalue=0, maxvalue=1600)),
        # Plot the wind power output
        plot(x=0.5:0.01:3.0,y=[w_out], Geom.line,
        Guide.XLabel("c_g1_scale"), Guide.YLabel("Dispatch of Wind, MW"),
        Scale.y_continuous(minvalue=0, maxvalue=250))
        )
    end
    

    For further reading on the impact of wind generation costs on dispatch decisions, we refer interested readers to J. M. Morales, A. J. Conejo, and J. Perez-Ruiz, "Economic Valuation of Reserves in Power Systems With High Penetration of Wind Power," IEEE Transactions on Power Systems, vol. 24, pp. 900-910, 2009.

    Modifying the JuMP model in place

    Note that in the previous exercise we entirely rebuilt the optimization model at every iteration of the internal loop, which incurs an additional computational burden. This burden can be alleviated if instead of re-building the entire model, we modify a specific constraint(s) or the objective function, as it shown in the example below.

    Compare the computing time in case of the above and below models.

    
    
    In [ ]:
    function solve_ed_inplace(c_w_scale)
        tic()
        obj_out = Float64[]
        w_out = Float64[]
        g1_out = Float64[]
        g2_out = Float64[]
        
        ed=Model() 
        
        # Define decision variables    
        @defVar(ed, 0 <= g[i=1:2] <= g_max[i]) # power output of generators
        @defVar(ed, 0 <= w  <= w_f ) # wind power injection
    
        # Define the objective function
        @setObjective(ed,Min,sum{c_g[i] * g[i],i=1:2}+ c_w * w)
    
        # Define the constraint on the maximum and minimum power output of each generator
        for i in 1:2
            @addConstraint(ed,  g[i] <= g_max[i]) #maximum
            @addConstraint(ed,  g[i] >= g_min[i]) #minimum
        end
    
    
        # Define the constraint on the wind power injection
        @addConstraint(ed, w <= w_f)
    
        # Define the power balance constraint
        @addConstraint(ed, sum{g[i], i=1:2} + w == d)
        solve(ed)
        
        for c_g1_scale = 0.5:0.01:3.0
            @setObjective(ed, Min, c_g1_scale*c_g[1]*g[1] + c_g[2]*g[2] + c_w_scale*c_w*w)
            solve(ed)
            push!(obj_out,getObjectiveValue(ed))
            push!(w_out,getValue(w))
            push!(g1_out,getValue(g[1]))
            push!(g2_out,getValue(g[2]))
        end
        toc()
        return obj_out, w_out, g1_out, g2_out
    end
    solve_ed_inplace(2.0);
    

    Adjusting specific constraints and/or the objective function is faster than re-building the entire model.

    A few practical limitations of the economic dispatch model

    Inefficient usage of wind generators

    The economic dispatch problem does not perform commitment decisions and, thus, assumes that all generators must be dispatched at least at their minimum power output limit. This approach is not cost efficient and may lead to absurd decisions. For example, if $ d = \sum_{i \in I} g^{\min}_{i}$, the wind power injection must be zero, i.e. all available wind generation is spilled, to meet the minimum power output constraints on generators.

    In the following example, we adjust the total demand and observed how it affects wind spillage.

    
    
    In [ ]:
    @manipulate for demandscale = 0.2:0.01:1.5
        g_opt,w_opt,ws_opt,obj = solve_ed(g_max, g_min, c_g, c_w, demandscale*d, w_f)
        
            html("Dispatch of Generators, MW: $(g_opt[:])<br>"*
        "Dispatch of Wind, MW: $w_opt<br>"*
        "Spillage of Wind, MW: $ws_opt<br>"*
        "Total cost, \$: $obj")
        
    end
    

    This particular drawback can be overcome by introducing binary decisions on the "on/off" status of generators. This model is called unit commitment and considered later in these notes.

    For further reading on the interplay between wind generation and the minimum power output constraints of generators, we refer interested readers to R. Baldick, "Wind and Energy Markets: A Case Study of Texas," IEEE Systems Journal, vol. 6, pp. 27-34, 2012.

    Transmission-infeasible solution

    The ED solution is entirely market-based and disrespects limitations of the transmission network. Indeed, the flows in transmission lines would attain the following values:

    $$f_{1-2} = 150 MW \leq f_{1-2}^{\max} = 100 MW $$$$f_{2-3} = 1200 MW \leq f_{2-3}^{\max} = 1000 MW $$

    Thus, if this ED solution was enforced in practice, the power flow limits on both lines would be violated. Therefore, in the following section we consider the optimal power flow model, which amends the ED model with network constraints.

    The importance of the transmission-aware decisions is emphasized in E. Lannoye, D. Flynn, and M. O'Malley, "Transmission, Variable Generation, and Power System Flexibility," IEEE Transactions on Power Systems, vol. 30, pp. 57-66, 2015.

    Unit Commitment model

    The Unit Commitment (UC) model can be obtained from ED model by introducing binary variable associated with each generator. This binary variable can attain two values: if it is "1", the generator is synchronized and, thus, can be dispatched, otherwise, i.e. if the binary variable is "0", that generator is not synchronized and its power output is set to 0.

    To obtain the mathematical formulation of the UC model, we will modify the constraints of the ED model as follows: $$ g^{\min}_{i} \cdot u_{t,i} \leq g_{i} \leq g^{\max}_{i} \cdot u_{t,i}, $$

    where $ u_{i} \in \{0;1\}. $ In this constraint, if $ u_{i} = 0$, then $g_{i} = 0$. On the other hand, if $ u_{i} = 1$, then $g^{max}_{i} \leq g_{i} \leq g^{min}_{i}$.

    For further reading on the UC problem we refer interested readers to G. Morales-Espana, J. M. Latorre, and A. Ramos, "Tight and Compact MILP Formulation for the Thermal Unit Commitment Problem," IEEE Transactions on Power Systems, vol. 28, pp. 4897-4908, 2013.

    In the following example we convert the ED model explained above to the UC model.

    
    
    In [ ]:
    # In this cell we introduce binary decision u to the economic dispatch problem (function solve_ed)
    function solve_uc (g_max, g_min, c_g, c_w, d, w_f)
    #Define the unit commitment (UC) model
    uc=Model() 
        
    # Define decision variables    
    @defVar(uc, 0 <= g[i=1:2] <= g_max[i]) # power output of generators
    @defVar(uc, u[i=1:2], Bin) # Binary status of generators
    @defVar(uc, 0 <= w  <= w_f ) # wind power injection
    
    # Define the objective function
    @setObjective(uc,Min,sum{c_g[i] * g[i],i=1:2}+ c_w * w)
    
    # Define the constraint on the maximum and minimum power output of each generator
    for i in 1:2
        @addConstraint(uc,  g[i] <= g_max[i] * u[i]) #maximum
        @addConstraint(uc,  g[i] >= g_min[i] * u[i]) #minimum
    end
    
    
    # Define the constraint on the wind power injection
    @addConstraint(uc, w <= w_f)
    
    # Define the power balance constraint
    @addConstraint(uc, sum{g[i], i=1:2} + w == d)
    
    # Solve statement
        status = solve(uc)
        
        return status, getValue(g), getValue(w), w_f-getValue(w), getValue(u), getObjectiveValue(uc)
    end
    
    # Solve the economic dispatch problem
    status,g_opt,w_opt,ws_opt,u_opt,obj=solve_uc (g_max, g_min, c_g, c_w, d, w_f);
    
      
    println("\n")
    println("Dispatch of Generators: ", g_opt[:], " MW")
    println("Commitments of Generators: ", u_opt[:])
    println("Dispatch of Wind: ", w_opt, " MW")
    println("Wind spillage: ", w_f-w_opt, " MW") 
    println("\n")
    println("Total cost: ", obj, "\$")
    

    Unit Commitment as a function of demand

    After implementing the UC model, we can now assess the interplay between the minimum power output constraints on generators and wind generation.

    
    
    In [ ]:
    @manipulate for demandscale = 0.2:0.01:1.5
        status, g_opt,w_opt,ws_opt, u_opt, obj = solve_uc(g_max, g_min, c_g, c_w, demandscale*d, w_f)
     
        if status == :Optimal
            html("Commitment of Generators, MW: $(u_opt[:])<br>"*
        "Dispatch of Generators, MW: $(g_opt[:])<br>"*
        "Dispatch of Wind, MW: $w_opt<br>"*
        "Spillage of Wind, MW: $ws_opt<br>"*
        "Total cost, \$: $obj")
        else
            html("Status: $status")
        end
    end
    

    Unit Commitment with different wind availability

    In the following experiment, we use a manipulator for adjusting demand and observe the different dispatch decisions under different wind generation conditions.

    
    
    In [ ]:
    @manipulate for demandscale = 0.2:0.05:1.45
        w_out = Float64[]
        g1_out = Float64[]
        g2_out = Float64[]
        
        for w_f_scale = 0.5:0.05:5
            status, g_opt,w_opt,ws_opt, u_opt, obj = solve_uc(g_max, g_min, c_g, c_w, demandscale*d, w_f*w_f_scale)
            push!(g1_out,g_opt[1])
            push!(g2_out,g_opt[2])
            push!(w_out,w_opt)
        end
        
        
            set_default_plot_size(16cm, 30cm)
        
        vstack(
        # Plot the power output of Generator 1
        plot(x=0.5:0.05:2,y=[g1_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of  G1, MW")),
        # Plot the power output of Generator 2    
        plot(x=0.5:0.05:5,y=[g2_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of  G2, MW")),
        # Plot the wind power output
        plot(x=0.5:0.05:5,y=[w_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of Wind, MW")),
        )
        
        
        
    end
    

    Unit Commitment with no load cost

    Like power output decisions ($g_i$), binary commitment decisions ($u_i$) can also be priced in the objective function. The physical interpretation of the cost incurred by binary commitment decisions is no-load component of the operating cost.

    This is implementing in the following example.

    
    
    In [ ]:
    # In this cell we redefine the UC model to account for the no-load cost
    
    function solve_uc_nlc (g_max, g_min, c_g, c_w, d, w_f, c_g0)
    #Define the unit commitment (UC) model
    uc=Model() 
        
    # Define decision variables    
    @defVar(uc, 0 <= g[i=1:2] <= g_max[i]) # power output of generators
    @defVar(uc, u[i=1:2], Bin) # Binary status of generators
    @defVar(uc, 0 <= w  <= w_f ) # wind power injection
    
    # Define the objective function
    @setObjective(uc,Min,sum{c_g[i] * g[i],i=1:2}+ c_w * w + sum{c_g0[i] * u[i],i=1:2})
    
    # Define the constraint on the maximum and minimum power output of each generator
    for i in 1:2
        @addConstraint(uc,  g[i] <= g_max[i] * u[i]) #maximum
        @addConstraint(uc,  g[i] >= g_min[i] * u[i]) #minimum
    end
    
    
    # Define the constraint on the wind power injection
    @addConstraint(uc, w <= w_f)
    
    # Define the power balance constraint
    @addConstraint(uc, sum{g[i], i=1:2} + w == d)
    
    # Solve statement
        status = solve(uc)
        
        return status, getValue(g), getValue(w), w_f-getValue(w), getValue(u), getObjectiveValue(uc)
    end
    

    Using the model above, we can now assess the sensitivity of the UC solution to demand under different levels of the minimum power output limits.

    
    
    In [ ]:
    @manipulate for demandscale = 0.2:0.05:1.45
        w_out = Float64[]
        g1_out = Float64[]
        g2_out = Float64[]
        
        for pmin_scale = 0.0:0.05:3
            status, g_opt,w_opt,ws_opt, u_opt, obj = solve_uc_nlc(g_max, pmin_scale*g_min, c_g, c_w, demandscale*d, w_f, c_g0)
            push!(g1_out,g_opt[1])
            push!(g2_out,g_opt[2])
            push!(w_out,w_opt)
        end
        
        
        set_default_plot_size(16cm, 30cm)
        
        vstack(
        # Plot the power output of Generator 1
        plot(x=0.5:0.05:2,y=[g1_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of  G1, MW")),
        # Plot the power output of Generator 2    
        plot(x=0.5:0.05:5,y=[g2_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of  G2, MW")),
        # Plot the wind power output
        plot(x=0.5:0.05:5,y=[w_out], Geom.line,
        Guide.XLabel("w_f_scale "), Guide.YLabel("Dispatch of Wind, MW")),
        )
        
        
    end